// --------------------------------------------------------------------------------------------------------------------
// <copyright file="AsyncEventTaskBinding.cs" company="JDML Consulting Ltd.">
//   Copyright 2011 Photon Project - http://photon.codeplex.com
// </copyright>
// <summary>
//   Defines the AsyncEventTaskBinding type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Photon.Threading.EventPattern
{
    using System;
    using System.ComponentModel;
    using System.Linq;
    using System.Reflection;

    public class AsyncEventTaskBinding : ITaskBinding
    {
        #region Fields

        private AsyncCallback _callBack;
        private readonly EventBinder<AsyncCompletedEventArgs> _eventBinder;
        private readonly object _target;
        private AsyncCompletedEventArgs _completedArgs;
        private readonly Action<object> _invoker;

        private const string StartMethodSuffix = "Async";
        private const string CompletedEventSuffix = "Completed";

        #endregion

        #region Factory Methods

        public static AsyncEventTaskBinding From<TArg1, TArg2, TArg3>(Action<TArg1, TArg2, TArg3, object> action, TArg1 arg1, TArg2 arg2,
            TArg3 arg3)
        {
            return new AsyncEventTaskBinding(action.Target, GetBinder(action),
                userState => action(arg1, arg2, arg3, userState));
        }

        public static AsyncEventTaskBinding From<TArg1, TArg2>(Action<TArg1, TArg2, object> action, TArg1 arg1, TArg2 arg2)
        {
            return new AsyncEventTaskBinding(action.Target, GetBinder(action),
                userState => action(arg1, arg2, userState));
        }

        public static AsyncEventTaskBinding From<TArg1>(Action<TArg1, object> action, TArg1 arg1)
        {
            return new AsyncEventTaskBinding(action.Target, GetBinder(action),
                userState => action(arg1, userState));
        }

        public static AsyncEventTaskBinding From(Action<object> action)
        {
            return new AsyncEventTaskBinding(action.Target, GetBinder(action), action);
        }

        public static AsyncEventTaskBinding<TEventArgs, TReturn> From<TEventArgs, TReturn>(
            Action<object> action, EventBinder<AsyncCompletedEventArgs> binder,
            Func<TEventArgs, TReturn> selector) where TEventArgs : AsyncCompletedEventArgs
        {
            return new AsyncEventTaskBinding<TEventArgs, TReturn>(action.Target, binder, action, selector);
        }

        public static AsyncEventTaskBinding<TEventArgs, TReturn> From<TArg1, TEventArgs, TReturn>(
            Action<TArg1, object> action, TArg1 arg1, EventBinder<AsyncCompletedEventArgs> binder,
            Func<TEventArgs, TReturn> selector) where TEventArgs : AsyncCompletedEventArgs
        {
            return new AsyncEventTaskBinding<TEventArgs, TReturn>(action.Target, binder, userState =>
                action(arg1, userState), selector);
        }

        private static AsyncEventTaskBinding<TEventArgs, TReturn> FromCore<TEventArgs, TReturn>(Delegate sourceAction,
            Action<object> invoker, Func<TEventArgs, TReturn> selector) where TEventArgs : AsyncCompletedEventArgs
        {
            return new AsyncEventTaskBinding<TEventArgs, TReturn>(sourceAction.Target,
                GetBinder(sourceAction), invoker, selector);
        }

        public static AsyncEventTaskBinding<TEventArgs, TReturn> From<TEventArgs, TReturn>(Action<object> action,
            Func<TEventArgs, TReturn> selector) where TEventArgs : AsyncCompletedEventArgs
        {
            return FromCore(action, action, selector);
        }

        public static AsyncEventTaskBinding<TEventArgs, TReturn> From<TArg1, TEventArgs, TReturn>(
            Action<TArg1, object> action, TArg1 arg1, Func<TEventArgs, TReturn> selector) where TEventArgs : AsyncCompletedEventArgs
        {
            return FromCore(action, userState => action(arg1, userState), selector);
        }

        public static AsyncEventTaskBinding<TEventArgs, TReturn> From<TArg1, TArg2, TEventArgs, TReturn>(
            Action<TArg1, TArg2, object> action, TArg1 arg1, TArg2 arg2, Func<TEventArgs, TReturn> selector)
            where TEventArgs : AsyncCompletedEventArgs
        {
            return FromCore(action, userState => action(arg1, arg2, userState), selector);
        }

        public static AsyncEventTaskBinding<TEventArgs, TReturn> From<TArg1, TArg2, TArg3, TEventArgs, TReturn>(
            Action<TArg1, TArg2, TArg3, object> action, TArg1 arg1, TArg2 arg2, TArg3 arg3, Func<TEventArgs, TReturn> selector)
            where TEventArgs : AsyncCompletedEventArgs
        {
            return FromCore(action, userState => action(arg1, arg2, arg3, userState), selector);
        }

        #endregion

        public AsyncEventTaskBinding(object target, EventBinder<AsyncCompletedEventArgs> binder, Action<object> invoker)
        {
            _target = target;
            _eventBinder = binder;
            _invoker = invoker;
        }

        public AsyncCompletedEventArgs CompletedArgs
        {
            get { return _completedArgs; }
        }

        public bool IsAsync
        {
            get { return true; }
        }

        public IAsyncResult Begin(AsyncCallback callBack)
        {
            _callBack = callBack;
            _eventBinder.AddHandler(_target, Completed);
            _invoker(this);
            return null;
        }

        public void End(IAsyncResult asyncResult)
        {
            _eventBinder.RemoveHandler(_target, Completed);
            if (CompletedArgs.Error != null)
            {
                throw CompletedArgs.Error;
            }
        }

        public void Completed(object sender, AsyncCompletedEventArgs args)
        {
            if (args.UserState == this)
            {
                _completedArgs = args;
                if (_callBack != null)
                {
                    _callBack(null);
                }
            }
        }

        private static EventBinder<AsyncCompletedEventArgs> GetBinder(Delegate action)
        {
            var method = action.Method;
            var eventName = GetCompletedEventName(method);

            // first look for explicit interface method
            EventInfo eventInfo = null;
            if (IsExplicitInterfaceMethod(method))
            {
                eventInfo = BindExplicitInterfaceEvent(method, eventName);
            }

            // now look for a standard binding on the declaring type
            if (eventInfo != null)
            {
                return EventBinder<AsyncCompletedEventArgs>.From(eventInfo);
            }

            // try again with standard binding
            eventInfo = BindEvent(method.ReflectedType, eventName);
            if (eventInfo != null)
            {
                return EventBinder<AsyncCompletedEventArgs>.From(eventInfo);
            }

            throw new ArgumentException(String.Format(ExceptionMessages.AsyncEventTaskBinding_BindingError,
                method.ReflectedType.Name, eventName), "action");
        }

        private static string GetCompletedEventName(MethodInfo method)
        {
            var methodName = method.Name;

            var dotPosition = methodName.LastIndexOf('.');
            if (dotPosition != -1)
            {
                methodName = methodName.Substring(dotPosition + 1);
            }

            if (methodName.EndsWith(StartMethodSuffix))
            {
                methodName = methodName.Substring(0, methodName.Length - StartMethodSuffix.Length);
            }

            methodName += CompletedEventSuffix;

            return methodName;
        }

        private static bool IsExplicitInterfaceMethod(MethodBase method)
        {
            return !method.IsStatic && method.IsPrivate && method.IsVirtual && method.IsFinal;
        }

        private static EventInfo BindExplicitInterfaceEvent(MethodInfo method, string eventName)
        {
            var type = method.ReflectedType;
            foreach (var intfType in type.GetInterfaces())
            {
                var map = type.GetInterfaceMap(intfType);
                if (map.TargetMethods.Any(m => m.Equals(method)))
                {
                    var eventInfo = BindEvent(intfType, eventName);
                    if (eventInfo != null)
                    {
                        return eventInfo;
                    }
                }
            }
            return null;
        }

        private static EventInfo BindEvent(Type targetType, string methodName)
        {
            var eventInfo = targetType.GetEvent(methodName);
            if (eventInfo == null)
            {
                return null;
            }

            var invokeMethod = eventInfo.EventHandlerType.GetMethod("Invoke");
            if (invokeMethod != null)
            {
                var parameters = invokeMethod.GetParameters();
                if (parameters.Length == 2 && typeof(AsyncCompletedEventArgs).IsAssignableFrom(parameters[1].ParameterType))
                {
                    return eventInfo;
                }
            }

            return null;
        }
    }

    public class AsyncEventTaskBinding<TArgs, TReturn> : AsyncEventTaskBinding, ITaskBinding<TReturn> where TArgs : AsyncCompletedEventArgs
    {
        #region Fields

        private readonly Func<TArgs, TReturn> _selector;

        #endregion

        protected internal AsyncEventTaskBinding(object target, EventBinder<AsyncCompletedEventArgs> binder, Action<object> invoker,
            Func<TArgs, TReturn> selector)
            : base(target, binder, invoker)
        {
            _selector = selector;
        }

        #region ITaskBinding<TReturn> Members

        public new TReturn End(IAsyncResult asyncResult)
        {
            base.End(asyncResult);
            return _selector != null ? _selector((TArgs)CompletedArgs) : default(TReturn);
        }

        #endregion
    }
}